﻿using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;
using System.Threading.Tasks;
using FastNet.Internal;

namespace FastNet
{
	/// <summary>
	/// Client for communication with FastServer.
	/// </summary>
	public sealed class FastClient
	{
		/// <summary>
		/// Creates a new client using the provided binding and address.
		/// </summary>
		/// <param name="binding">The underlying binding to use for the channel.</param>
		/// <param name="address">The remote address where the client will be connecting to.</param>
		/// <returns></returns>
		public static FastClient Create( Binding binding, EndpointAddress address )
		{
			Logic.Check<ArgumentNullException>( binding != null, "binding" );
			Logic.Check<ArgumentNullException>( address != null, "address" );
			
			return Create( binding, address, null );
		}


		/// <summary>
		/// Creates a new client using the provided binding, address and optionally a synchronization context.
		/// </summary>
		/// <param name="binding">The underlying binding to use for the channel.</param>
		/// <param name="address">The remote address where the client will be connecting to.</param>
		/// <param name="context">The synchronization context used for dispatching events. Can be null.</param>
		public static FastClient Create( Binding binding, EndpointAddress address, SynchronizationContext context )
		{
			Logic.Check<ArgumentNullException>( binding != null, "binding" );
			Logic.Check<ArgumentNullException>( address != null, "address" );
						
			FastClientCallbackChannel	callbackInstance	= new FastClientCallbackChannel();
			ICommandChannel				channel				= DuplexChannelFactory<ICommandChannel>.CreateChannel( new InstanceContext( callbackInstance ), binding, address );
			FastClient					client				= new FastClient( channel, callbackInstance );

			client.m_binding = binding;
			client.m_address = address;
			client.m_context = context;

			callbackInstance.Associate( client );

			return client;
		}


		private FastClient( ICommandChannel channel, FastClientCallbackChannel callbackInstance )
		{
			Logic.Check<ArgumentNullException>( channel != null, "channel" );
			Logic.Check<ArgumentNullException>( callbackInstance != null, "callbackInstance" );

			( (ICommunicationObject)channel ).Closed	+= new EventHandler( OnChannelClosed );
			( (ICommunicationObject)channel ).Faulted	+= new EventHandler( OnChannelFaulted );
			
			m_channel			= channel;
			m_callbackChannel	= callbackInstance;
		}


		/// <summary>
		/// Returns the underlying state of the channel.
		/// </summary>
		public CommunicationState State
		{
			get
			{
				return ( (ICommunicationObject)m_channel ).State;
			}
		}


		/// <summary>
		/// Returns whether the underlying channel is opened.
		/// </summary>
		public Boolean IsConnected
		{
			get
			{
				return this.State == CommunicationState.Opened;
			}
		}

		
		/// <summary>
		/// Any messages received during the session are dispatched for processing in this event.
		/// </summary>
		public event EventHandler<MessageEventArgs> ProcessMessage;


		/// <summary>
		/// Raised when the underlying channel has been closed.
		/// </summary>
		public event EventHandler Disconnected;


		/// <summary>
		/// Opens the connection to the server. This should be called before start sending messages.
		/// </summary>
		/// <exception cref="EndpointNotFoundException" />
		public void Open( String username, String password )
		{
			lock ( m_lock )
			{
				if ( this.State != CommunicationState.Created )
				{
					throw new InvalidOperationException( "This client has already been opened. You need to reset it in order to reopen it." );
				}

				m_channel.Open( username, password );

				m_evMessages		= new AutoResetEvent( false );
				m_evRegistration	= ThreadPool.RegisterWaitForSingleObject( m_evMessages, this.Process, null, KEEP_ALIVE_INTERVAL, true );
			}
		}


		/// <summary>
		/// Opens the connection to the server. This should be called before start sending messages.
		/// </summary>
		/// <exception cref="EndpointNotFoundException" />
		public Task OpenAsync( String username, String password )
		{
			return Task.Factory.StartNew( () =>
			{
				Open( username, password );
			} );
		}


		/// <summary>
		/// Resents the underlying channel to initial non connected state. If the channel is still connected the closed event is not rised.
		/// After calling this method, you can connect again to the server using the Open method.
		/// </summary>
		public void Reset()
		{
			lock ( m_lock )
			{
				Close();

				var	callbackInstance	= new FastClientCallbackChannel();
				var	channel				= DuplexChannelFactory<ICommandChannel>.CreateChannel( new InstanceContext( callbackInstance ), m_binding, m_address );

				callbackInstance.Associate( this );

				( (ICommunicationObject)channel ).Closed += new EventHandler( OnChannelClosed );
				( (ICommunicationObject)channel ).Faulted += new EventHandler( OnChannelFaulted );

				m_channel = channel;
				m_callbackChannel = callbackInstance;
			}
		}


		/// <summary>
		/// Sends a message to the server.
		/// </summary>
		/// <param name="message">The message. Cannot be null.</param>
		/// <returns>True if the send was successful, false to indicate that the client is not connected.</returns>
		public Boolean Send( FastMessage message )
		{
			Logic.Check<InvalidOperationException>( OperationContext.Current == null, "You are not allowed to send message in an existing operation context." );
			Logic.Check<ArgumentNullException>( message != null, "message" );

			Byte[] data = message.GetData();

			lock ( m_lock )
			{
				if ( this.State == CommunicationState.Opened )
				{
					try
					{
						m_channel.Send( data );

						return true;
					}
					catch ( CommunicationException )
					{
						this.Close();
					}
				}
			}

			return false;
		}


		/// <summary>
		/// Transitions the underlying channel in closed state. To use the client you must reset it first.
		/// The disconnected event is not rised.
		/// </summary>
		public void Close()
		{
			lock ( m_lock )
			{
				( (ICommunicationObject)m_channel ).Closed -= OnChannelClosed;
				( (ICommunicationObject)m_channel ).Faulted -= OnChannelFaulted;

				if ( m_evRegistration != null )
				{
					m_evRegistration.Unregister( m_evMessages );
					m_evRegistration = null;

					m_evMessages.Dispose();
					m_evMessages = null;
				}

				if ( this.State <= CommunicationState.Opened )
				{
					try
					{
						( (ICommunicationObject)m_channel ).Close();
					}
					catch
					{
					}
				}

				if ( m_callbackChannel != null )
				{
					m_callbackChannel.Dispose();
					m_callbackChannel = null;
				}

				m_messages.Clear();
			}
		}


		internal void ReceiveSend( Byte[] data )
		{
			FastMessage	message = FastMessage.ReadFrom( data );

			lock ( m_lock )
			{
				if ( m_evMessages != null )
				{
					if ( message.Command != FastCommands.KeepAlive )
					{
						m_messages.Enqueue( message );
					}

					m_evMessages.Set();
				}
			}
		}


		private void OnChannelClosed( Object sender, EventArgs e )
		{
			this.Disconnected.Raise( this, EventArgs.Empty, m_context );
		}


		private void OnChannelFaulted( Object sender, EventArgs e )
		{
			this.Disconnected.Raise( this, EventArgs.Empty, m_context );
		}


		private void Process( Object state, Boolean timedOut )
		{
			if ( timedOut )
			{
				Send( new FastMessage( FastCommands.KeepAlive ) );
			}

			FastMessage message = null;

			while ( true )
			{
				lock ( m_lock )
				{
					if ( m_messages.Count > 0 )
					{
						message = m_messages.Dequeue();
					}
					else
					{
						if ( m_evMessages != null )
						{
							m_evRegistration = ThreadPool.RegisterWaitForSingleObject( m_evMessages, this.Process, null, KEEP_ALIVE_INTERVAL, true );
						}

						break;
					}
				}

				if ( message != null )
				{
					this.ProcessMessage.Raise( this, new MessageEventArgs( message ), m_context );
				}
			}
		}


		private readonly Object					m_lock				= new Object();
		private AutoResetEvent					m_evMessages		= null;
		private RegisteredWaitHandle			m_evRegistration	= null;
		private readonly Queue<FastMessage>		m_messages			= new Queue<FastMessage>();
		private ICommandChannel					m_channel			= null;
		private FastClientCallbackChannel		m_callbackChannel	= null;
		private Binding							m_binding			= null;
		private EndpointAddress					m_address			= null;
		private SynchronizationContext			m_context			= null;
		private const Int32						KEEP_ALIVE_INTERVAL = 30 * 1000; // 30 Seconds. If no message is received in that period initiate keep alive message.
	}
}
